Skip to content

[FEAT] 특약 로직 수정 및 AI 메시지 추가#78

Merged
MeongW merged 31 commits intodevelopfrom
feature/chat
Aug 13, 2025
Merged

[FEAT] 특약 로직 수정 및 AI 메시지 추가#78
MeongW merged 31 commits intodevelopfrom
feature/chat

Conversation

@leeedongjaee
Copy link
Contributor

@leeedongjaee leeedongjaee commented Aug 13, 2025

🚀 관련 이슈

특약 로직 수정 및 AI 메시지 추가

#37

🔑 주요 변경사항

  • 각 단계 별 AI 메시지 로직 추가
  • 사전조사 관련 메시지 추가

✔️ 체크 리스트

  • Merge 하려는 브랜치가 올바른가? (main branch에 실수로 PR 생성 금지)
  • Merge 하려는 PR 및 Commit들을 로컬에서 실행했을 때 에러가 발생하지 않았는가?
  • 라벨을 등록했는가?
  • 리뷰어를 지정했는가?

📢 To Reviewers

📸 스크린샷 or 실행영상

↗️ 개선 사항

Summary by CodeRabbit

  • New Features

    • 최종 계약 워크플로 확장: 조항 수정 요청/응답, 삭제 요청/응답, 확정 요청/수락, 대기 요청 조회 및 계약 상태 조회
    • 미완료 특약(메시지 없는 항목) 조회, 전체 라운드 조회 및 자동 라운드 진행, 계약채팅 이동용 URL 제공
    • 계약 생성/저장 시 프론트용 링크 자동 전송 및 AI 알림/버튼 메시지 트리거
  • Refactor

    • AI 메시지 전송에 순서(order) 파라미터 적용 및 미리보기 개선
    • 계약 링크를 URLLINK 타입으로 통일하고 프론트 URL 구성 방식 도입
    • 계약 상태에 ROUND4 추가 및 소유자/접근 검증 강화
  • Chores

    • 서브모듈 포인터(메타데이터) 업데이트

@coderabbitai
Copy link

coderabbitai bot commented Aug 13, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

최종 계약(수정·삭제·확정) 및 특약(스페셜) 라운드 관련 다수의 API·서비스·레포지토리·DTO 추가·변경, ContractStatus에 ROUND4 추가, 프리컨트랙트 저장 시 채팅으로 URLLINK 전송 로직과 서브모듈 포인터 갱신이 포함됩니다.

Changes

Cohort / File(s) Change Summary
Controllers (API 확장)
src/main/java/org/scoula/domain/chat/controller/ContractChatController.java, src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
최종 계약 수정/응답/삭제/확정 관련 다수 엔드포인트 추가·구현, 특약 미완료 조회·자동진행·상태조회·채팅방 이동 URL 노출 등; AI 메시지 전송 시 order 파라미터 시그니처 추가 및 접근 검증 강화.
Service Interface / Impl (도메인 로직 확장)
src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java, src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
수정요청/응답, 삭제요청/응답, 확정요청/수락/거절, 상태 파라미터·URL 제공, Redis 기반 임시 상태 관리, 최종 문서 조립 및 라운드/상태 진행 로직 등 대규모 기능 추가.
Chat Service 변경
src/main/java/org/scoula/domain/chat/service/ChatServiceInterface.java, src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java
계약 채팅방 사용자 조회(isUserInContractChatRoom) 추가; 프론트 base URL 주입으로 계약 링크 조립 변경; 계약 수락 메시지 타입을 URLLINK로 전송하도록 수정.
DTOs 추가
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java, src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java, src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java, src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java
최종 계약 관련 요청·응답·임시 데이터 보관용 DTO 4종 추가(Lombok 사용).
Repository 변경
src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java
기존 다수 조회/삭제 메서드 제거; recentData.messages가 비어있는 특약 문서만 조회하는 findByContractChatIdAndIsPassedAndRecentDataMessagesEmpty 전용 메서드 추가.
VO/Enum
src/main/java/org/scoula/domain/chat/vo/ContractChat.java
ContractStatus에 ROUND4 추가 및 getCurrentRound/isInRound 로직 보완.
Precontract 서비스 통합
src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java, src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java
Mongo 저장 후 채팅으로 URLLINK 전송 및 AI 메시지 트리거 추가; 채팅·컨트랙트 서비스 등 의존성 추가 주입 및 호출.
Contract 서비스 변경
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
계약 저장 권한 검증을 소유자 기준 validateIsOwner로 변경 및 해당 메서드 추가.
Mapper / SQL 추가
src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java, src/main/resources/.../TenantPreContractMapper.xml
contract owner id 조회용 selectContractOwnerId 쿼리·메서드 추가.
Submodule pointer
config-submodule
서브모듈 커밋 포인터 갱신(메타데이터 변경).

Sequence Diagram(s)

sequenceDiagram
  actor Owner
  participant Controller as ContractChatController
  participant Service as ContractChatService
  participant Redis as RedisStore
  participant DB as Mongo/DB

  Owner->>Controller: POST /final-contract/{id}/modification-request
  Controller->>Service: requestFinalContractModification(id, ownerId, dto)
  Service->>Redis: 저장(수정 요청 데이터)
  Service-->>Controller: ModificationRequestData
  Controller-->>Owner: ApiResponse(ModificationRequestData)

  Buyer->>Controller: POST /final-contract/{id}/modification-response
  Controller->>Service: respondToModificationRequest(id, buyerId, responseDto)
  Service->>Redis: 조회·삭제(대기중 요청)
  alt accepted
    Service->>DB: 최종문서 적용/갱신
  end
  Service-->>Controller: FinalSpecialContractDocument
  Controller-->>Buyer: ApiResponse(FinalSpecialContractDocument)
Loading
sequenceDiagram
  actor Owner
  actor Buyer
  participant Controller as ContractChatController
  participant Service as ContractChatService
  participant Redis as RedisStore
  participant DB as Mongo/DB

  Owner->>Controller: POST /{id}/final-contract/request-confirmation
  Controller->>Service: requestFinalContractConfirmation(id, ownerId)
  Service->>Redis: set(confirm:pending)
  Service-->>Controller: ok

  Buyer->>Controller: POST /{id}/final-contract/accept-confirmation
  Controller->>Service: acceptFinalContractConfirmation(id, buyerId, responseDto)
  Controller->>Service: hasPendingModificationRequest/ownership 체크
  Service->>Redis: 검증(confirm:pending)
  Service->>DB: 상태 업데이트(예: ROUND4 등) 및 알림
  Service-->>Controller: Map(result)
  Controller-->>Buyer: ApiResponse(Map)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
계약 전 채팅 로직 구현, 계약 전 채팅 API 구현 (#1)
SSE 활용한 채팅 알림 구현 (#1) SSE(Server-Sent Events) 관련 엔드포인트/구현이 변경 내에 없음.
채팅 리스트 API 구현 (#1) 채팅방 리스트 전용 API 추가 여부가 변경 목록에서 명확하지 않음.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
최종 계약 관리 워크플로우 대규모 추가 (예: src/main/java/org/scoula/domain/chat/controller/ContractChatController.java, .../ContractChatServiceImpl.java) 링크된 이슈(#1)는 주로 계약 전 채팅(실시간 메시징, 채팅 리스트, SSE 알림 등)을 요구하는 반면, 이번 변경은 최종 계약의 수정·삭제·확정 라이프사이클 구현으로 이슈 범위와 직접적 일치성이 낮음.
SpecialContractMongoRepository 기존 다수 메서드 제거 (src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java) 채팅 모듈 요구사항과 직접 관련되지 않는 데이터 접근 레이어의 기능 삭제/리팩토링이 포함되어 있어 이슈의 목표와 일치하지 않음.
ContractStatus에 ROUND4 추가 (src/main/java/org/scoula/domain/chat/vo/ContractChat.java) 채팅 기능 요구사항이 아니라 계약 단계 확장(비즈니스 로직 변경)에 해당하여 이슈 범위를 벗어날 가능성이 높음.

Possibly related PRs

Suggested labels

✨ feature

Suggested reviewers

  • Whatdoyumin
  • MeongW

Poem

"나는 토끼, 깡충 뛰며 알리네,
조항에 깃발 꽂고 수정도 척척,
링크 한 줄 띄워 방을 열고,
라운드가 쌓이면 ROUND4에 환호,
깡총토끼가 축하해요 — 축하 폴짝! 🐇"


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c025732 and eef8835.

📒 Files selected for processing (1)
  • src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java (4 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/chat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🔭 Outside diff range comments (2)
src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (1)

636-651: isUserInContractChatRoom 접근 체크 로직 불일치 가능성

현재 구현은 userCurrentChatRoom(일반 채팅방 ID를 담는 맵)에서 contractChatId와 비교합니다. 일반/계약 채팅방 ID 네임스페이스가 다르다면 항상 false가 나올 수 있습니다. 계약 채팅방 입장/퇴장 시점을 별도로 기록하거나, 공용 맵을 사용하더라도 동일 ID 체계를 보장해야 합니다.

제안:

  • 별도의 맵 도입: Map<Long, Long> userCurrentContractChatRoom
  • 입장/퇴장 트래킹 메서드 추가: setUserCurrentContractChatRoom/removeUserFromCurrentContractChatRoom
  • isUserInContractChatRoom은 해당 맵을 참조
  • 혹은 기존 맵을 재사용하려면 저장하는 ID가 항상 "계약 채팅방 ID"가 되도록 호출부 전반을 정렬

원하시면 적용 패치를 제안드리겠습니다.

src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (1)

1596-1724: 복잡한 메서드를 더 작은 단위로 분리하세요

saveFinalSpecialContract 메서드가 130줄이 넘어 너무 깁니다. 로직을 더 작은 메서드로 분리하면 가독성과 테스트 용이성이 향상됩니다.

+    private FinalSpecialContractDocument.FinalClause findValidClauseFromRounds(
+            Long contractChatId, Integer clauseOrder, Long startRound) {
+        for (Long searchRound = startRound; searchRound >= 1L; searchRound--) {
+            Optional<SpecialContractDocument> docOpt = 
+                specialContractMongoRepository.findSpecialContractDocumentByContractChatIdAndRound(
+                    contractChatId, searchRound);
+            
+            if (docOpt.isPresent()) {
+                Optional<SpecialContractDocument.Clause> clauseOpt = 
+                    findValidClauseInDocument(docOpt.get(), clauseOrder);
+                
+                if (clauseOpt.isPresent()) {
+                    SpecialContractDocument.Clause clause = clauseOpt.get();
+                    log.info("특약 {}번 저장 완료 (라운드 {}): {}", clauseOrder, searchRound, clause.getTitle());
+                    return FinalSpecialContractDocument.FinalClause.builder()
+                        .order(clauseOrder)
+                        .title(clause.getTitle().trim())
+                        .content(clause.getContent().trim())
+                        .build();
+                }
+            }
+        }
+        return null;
+    }
+    
+    private Optional<SpecialContractDocument.Clause> findValidClauseInDocument(
+            SpecialContractDocument doc, Integer clauseOrder) {
+        return doc.getClauses().stream()
+            .filter(clause -> clause.getOrder() != null && clause.getOrder().equals(clauseOrder))
+            .filter(clause -> isValidClause(clause))
+            .findFirst();
+    }
+    
+    private boolean isValidClause(SpecialContractDocument.Clause clause) {
+        return clause.getTitle() != null && !clause.getTitle().trim().isEmpty()
+            && clause.getContent() != null && !clause.getContent().trim().isEmpty();
+    }
♻️ Duplicate comments (3)
src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (1)

10-10: 와일드카드 임포트 사용을 지양하세요

Controller와 동일하게 와일드카드 임포트를 사용하고 있습니다. 명시적 임포트를 사용하는 것이 좋습니다.

src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java (2)

12-12: 와일드카드 임포트 사용을 지양하세요


903-903: 일관성 없는 파라미터 타입

Controller 인터페이스의 주석과 동일하게, FinalContractDeletionResponseDto가 확정 수락 메서드에 사용되고 있습니다.

🧹 Nitpick comments (7)
config-submodule (1)

1-1: 메시징/알림 트랜잭션 경계·재시도·아이도템포턴시 고려

프리컨트랙트 저장 직후 URL 링크 메시지 전송 로직이 포함된 것으로 보입니다. 다음을 반영하면 신뢰성이 올라갑니다.

  • 트랜잭션 커밋 이후 비동기 발송(트랜잭션 동기화 이벤트/아웃박스 패턴).
  • 메시지 아이디 기반 중복 방지(Idempotency-Key).
  • SSE 재연결/오프라인 백필 전략, WebSocket 구독 권한 재검증.
  • 장애 시 재시도(지수 백오프)와 데드레터 채널 운영.
src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java (1)

5-13: Lombok 애노테이션 중복 제거 및 스타일 일관성

@DaTa와 @Getter/@Setter를 동시에 사용하고 있습니다. 중복이며, 다른 DTO(FinalContractModificationResponseDto)와 스타일도 다릅니다. 한 쪽만 유지하는 게 좋습니다. 아래처럼 @DaTa를 제거해 ModificationResponseDto와 맞추길 권장합니다.

-@Data
 @Builder
 @AllArgsConstructor
 @NoArgsConstructor
 @Getter
 @Setter
 public class FinalContractDeletionResponseDto {
     private boolean accepted;
 }
src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java (4)

247-249: 메시지 비존재 미완료 특약 조회 메서드의 스키마/인덱스 가정 명시 필요

getIncompleteSpecialContractsWithoutMessage의 "message 없음" 정의(필드 미존재 vs 공백 문자열 vs 빈 배열)를 명확히 해주세요. 또한 repository 레벨에서 해당 조건을 효율적으로 처리하기 위한 인덱스 전략을 문서/주석로 남기는 것을 권장합니다.


265-266: AI 메시지 메서드 네이밍/역할 정의 통일

AiMessage, AiMessageBtn, AiMessageNext의 역할/차이를 인터페이스 주석으로 명확히 해주세요. 호출 순서 보장, 버튼 payload 스펙 등도 함께 정의되면 구현체/호출부 정합성 유지에 도움이 됩니다.


267-276: 최종 특약 수정 요청/응답 API 계약 명확화 및 예외 규격화

역할(임대인만 요청 가능, 임차인만 응답 가능), 중복 요청 방지, 동시성 처리(동일 조항 동시 요청)에 대한 계약을 자바독으로 명시하고, 권한/상태 오류 시 던지는 예외 타입을 통일해 주세요.

예시 자바독(패턴 제안):

/**
 * 임대인이 특정 조항에 대해 최종 특약 수정 요청을 등록합니다.
 *
 * 권한: owner만 호출 가능
 * 전제: 해당 contractChatId가 존재하고, 최종 특약 확정 전 상태여야 함
 * 제약: 동일 clauseOrder에 대해 pending 요청이 이미 존재하면 BusinessException
 *
 * @throws IllegalArgumentException null 파라미터
 * @throws BusinessException 권한 불일치, 상태 불일치, 중복 요청
 */

277-304: Map 반환 타입을 전용 DTO로 치환 고려

acceptFinalContractConfirmation, acceptFinalContractDeletion, respondToFinalContractDeletionRequest 등이 Map<String, Object>를 반환하고 있습니다. 스키마가 불명확해 프론트/호출부에서 타입 안정성이 떨어집니다. 전용 DTO를 도입해 계약을 명확히 해주세요.

예: FinalContractActionResultDto { status, message, updatedDocumentId, ... }

src/main/java/org/scoula/domain/chat/controller/ContractChatController.java (1)

10-10: 와일드카드 임포트 사용을 지양하세요

org.scoula.domain.chat.dto.*와 같은 와일드카드 임포트 대신 필요한 클래스를 명시적으로 임포트하는 것이 좋습니다. 이는 코드의 가독성을 높이고 사용되는 클래스를 명확히 파악할 수 있게 합니다.

-import org.scoula.domain.chat.dto.*;
+import org.scoula.domain.chat.dto.ContractChatMessageRequestDto;
+import org.scoula.domain.chat.dto.SpecialContractUserViewDto;
+import org.scoula.domain.chat.dto.FinalContractModificationRequestDto;
+import org.scoula.domain.chat.dto.FinalContractModificationResponseDto;
+import org.scoula.domain.chat.dto.FinalContractDeletionResponseDto;
+import org.scoula.domain.chat.dto.ModificationRequestData;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81d3797 and fa09a0f.

📒 Files selected for processing (14)
  • config-submodule (1 hunks)
  • src/main/java/org/scoula/domain/chat/controller/ContractChatController.java (3 hunks)
  • src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java (6 hunks)
  • src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java (1 hunks)
  • src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java (1 hunks)
  • src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (1 hunks)
  • src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java (1 hunks)
  • src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java (1 hunks)
  • src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (1 hunks)
  • src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (12 hunks)
  • src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java (3 hunks)
  • src/main/java/org/scoula/domain/chat/vo/ContractChat.java (1 hunks)
  • src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java (3 hunks)
  • src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java (3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
src/main/java/org/scoula/**/service/*Service{Interface,Impl}.java

📄 CodeRabbit Inference Engine (CLAUDE.md)

Use interface-implementation pattern for services: *ServiceInterface.java (interface) and *ServiceImpl.java (implementation)

Files:

  • src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java
  • src/main/java/org/scoula/domain/chat/service/ContractChatServiceInterface.java
  • src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java
  • src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java
  • src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
src/main/java/org/scoula/domain/*/repository/*Repository.java

📄 CodeRabbit Inference Engine (CLAUDE.md)

Place MongoDB repositories under src/main/java/org/scoula/domain/*/repository/*Repository.java

Files:

  • src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java
src/main/java/org/scoula/**/controller/*Controller{,Impl}.java

📄 CodeRabbit Inference Engine (CLAUDE.md)

Use interface-implementation pattern for controllers: *Controller.java (interface) and *ControllerImpl.java (implementation)

Files:

  • src/main/java/org/scoula/domain/chat/controller/ContractChatController.java
  • src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
src/main/java/org/scoula/**/controller/**/*.java

📄 CodeRabbit Inference Engine (CLAUDE.md)

src/main/java/org/scoula/**/controller/**/*.java: All controller endpoints should return a consistent ApiResponse<T> wrapper
Add Swagger annotations to controller endpoints for API documentation

Files:

  • src/main/java/org/scoula/domain/chat/controller/ContractChatController.java
  • src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
🧠 Learnings (1)
📚 Learning: 2025-08-12T03:59:30.512Z
Learnt from: CR
PR: ITZEEP/backend#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-12T03:59:30.512Z
Learning: Applies to src/main/java/org/scoula/domain/*/repository/*Repository.java : Place MongoDB repositories under `src/main/java/org/scoula/domain/*/repository/*Repository.java`

Applied to files:

  • src/main/java/org/scoula/domain/chat/repository/SpecialContractMongoRepository.java
🧬 Code Graph Analysis (4)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (2)
src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java (1)
  • Getter (5-15)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java (1)
  • Getter (5-14)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java (2)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (1)
  • Getter (5-12)
src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java (1)
  • Getter (5-15)
src/main/java/org/scoula/domain/chat/dto/FinalContractDeletionResponseDto.java (1)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (1)
  • Getter (5-12)
src/main/java/org/scoula/domain/chat/dto/ModificationRequestData.java (2)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (1)
  • Getter (5-12)
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java (1)
  • Getter (5-14)
🔇 Additional comments (8)
config-submodule (4)

1-1: Submodule 포인터 업데이트 자체는 적절합니다

상위 리포에서 서브모듈 커밋 포인터만 변경된 메타데이터 변경이며, 이 파일 단위로는 문제 없어 보입니다.


1-1: 영향 범위 확인 완료: 제거된 MongoRepository 퍼블릭 API 호출 없음

config-submodule을 제외한 전체 코드베이스에서 아래 제거된 메서드에 대한 호출이 전혀 발견되지 않았습니다.
빌드 타임 컴파일 오류 가능성이 없으므로 별도 조치가 필요 없습니다.


1-1: 모듈 경로 수정 및 컨트롤러 보안 애너테이션 검증 안내
config-submodule에는 Java 컨트롤러가 없으므로, 실제 컨트롤러 엔드포인트 검증 대상은 메인 리포지토리의 src/main/java/org/scoula/domain/chat/controller 입니다. 아래 스크립트로 @PreAuthorize/@Secured/@RolesAllowed 누락 매핑을 확인하세요.

• 점검 대상 파일

  • src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
  • src/main/java/org/scoula/domain/chat/controller/ChatControllerImpl.java
#!/bin/bash
set -euo pipefail

echo "클래스 레벨 보안 애너테이션 검사"
rg -n '@(PreAuthorize|Secured|RolesAllowed)' src/main/java/org/scoula/domain/chat/controller || echo "  → 보안 애너테이션 미사용 클래스 없음"

echo
echo "매핑 라인 및 직전 컨텍스트 출력"
rg -n -B 3 '@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\(' src/main/java/org/scoula/domain/chat/controller | sed 's/^/  /' || true

이 결과에서 보안 애너테이션이 없는 매핑이 발견되면 즉시 추가 적용을 권장합니다.
또한 SSE/WebSocket 엔드포인트가 있다면 토큰 검증, 구독 권한, CORS/캐시 제어도 별도 점검해 주세요.

Likely an incorrect or invalid review comment.


1-1: ContractStatus에 ROUND4 추가로 인한 분기 누락 위험 점검 필요
현재 config-submodule 내에서 ContractStatus enum 정의나 처리 분기 로직(switch/when/if)을 찾지 못했습니다.

  • 해당 enum 및 분기 처리 코드가 위치한 파일 경로를 알려주시거나 직접 위치를 확인해 주세요.
  • ROUND4가 모든 상태 전이 분기 경로에 포함되어 있는지 재검토 바랍니다.
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationResponseDto.java (1)

1-12: 단순 수락/거절 응답 DTO: OK

필드/롬복 조합이 간결하고 용도에 부합합니다. 일관된 네이밍(accepted)도 좋습니다.

src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (1)

830-831: 계약 링크 발신자를 소유자로 설정: 타당

계약을 수락한 주체(소유자)가 링크를 발송하도록 변경된 점 합리적입니다.

src/main/java/org/scoula/domain/chat/controller/ContractChatController.java (1)

145-147: order 파라미터 필수화에 따른 클라이언트 영향 검증 필요

sendAiMessage 메서드가 이제 @RequestParam Long order필수 로 요구합니다.
백엔드 코드베이스 내 검색 결과, order 없이 호출되는 곳은 다음 두 군데 선언부뿐이어서 내부 컴파일 오류는 없지만, 외부(프론트엔드·모바일·API 소비자 등)에서 호출 시에는 breaking change가 될 수 있습니다.

검증할 사항:

  • 프론트엔드/모바일/외부 API 스펙에 order 파라미터 추가 여부 확인
  • 레거시 호출을 그대로 지원해야 한다면 required=false 또는 defaultValue 지정 고려
src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (1)

1013-1014: ROUND4 상태 처리 확인 완료

  • src/main/java/org/scoula/domain/chat/vo/ContractChat.java
    ContractStatus enum에 ROUND4 항목이 정의되어 있습니다.
  • src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
    contractChatMapper.updateStatus(..., ContractChat.ContractStatus.ROUND4) 호출(3곳)
    switch문 내 case ROUND4: 분기 처리("?step=3&round=4" 반환)

위 검증 결과, ROUND4 상태는 enum 정의부터 서비스 로직, 매퍼 호출, 분기 처리까지 시스템 전반에서 정상적으로 지원되고 있어 추가 조치가 필요하지 않습니다.

config-submodule Outdated
@@ -1 +1 @@
Subproject commit 821c2c2ab5a06ba5e9f3e7ed6f23041d5c36bbec
Subproject commit 2d91d5b967d71c3c6a3a658d296a1453141a3c24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

DTO 입력값 검증 강화(@Valid, Bean Validation) 제안

신규 DTO(예: FinalContractModificationRequestDto, ModificationRequestData 등)에 @NotNull/@SiZe 등 제약을 부여하고, 컨트롤러 메서드에서 @Valid를 적용하세요. 유효성 검증 실패 시 표준 에러 응답 포맷을 일관 제공하는 것이 좋습니다.

예시:

// DTO
@Getter @Setter
public class FinalContractModificationRequestDto {
  @NotNull
  private Integer clauseOrder;
  @NotBlank @Size(max = 100)
  private String newTitle;
  @NotBlank @Size(max = 5000)
  private String newContent;
}

// Controller
@PostMapping("/final-contract/{contractChatId}/modification")
public ResponseEntity<?> requestModification(
    @PathVariable Long contractChatId,
    @Valid @RequestBody FinalContractModificationRequestDto dto) { ... }
🤖 Prompt for AI Agents
In config-submodule around lines 1-1, DTOs like
FinalContractModificationRequestDto and ModificationRequestData currently lack
Bean Validation annotations and controllers are not using @Valid; add
appropriate javax.validation constraints (@NotNull, @NotBlank, @Size, etc.) to
DTO fields and annotate controller @RequestBody parameters with @Valid, and
implement a global @ControllerAdvice (or extend existing exception handler) to
catch MethodArgumentNotValidException and ConstraintViolationException to return
a consistent error response payload and HTTP 400 status. Ensure DTO fields have
clear constraint messages where useful and update any unit/integration tests to
assert validation behavior.

💡 Verification agent

🧩 Analysis chain

서브모듈 커밋 유효성(.gitmodules 매핑, 원격 커밋 존재, 체크아웃 상태) 검증 권장

원격에 해당 커밋이 실제로 존재하는지, .gitmodules 설정과 워킹트리 체크아웃이 일치하는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

commit="2d91d5b967d71c3c6a3a658d296a1453141a3c24"
name="config-submodule"

echo "[1/5] .gitmodules에서 $name URL 확인"
test -f .gitmodules || { echo "ERROR: .gitmodules 없음"; exit 1; }
url="$(git config -f .gitmodules --get submodule.${name}.url || true)"
echo "submodule URL: ${url:-<empty>}"
test -n "${url:-}" || { echo "ERROR: ${name} URL 누락"; exit 1; }

echo "[2/5] 원격에 커밋 존재 여부 확인"
git ls-remote "$url" | grep -q "$commit" && echo "OK: 원격에 커밋 존재" || { echo "ERROR: 원격에 커밋 미존재"; exit 1; }

echo "[3/5] 서브모듈 초기화/업데이트"
git submodule update --init "$name"

echo "[4/5] 서브모듈 HEAD 확인"
( cd "$name" && git rev-parse HEAD )

echo "[5/5] 상위 포인터와 일치 여부 확인"
actual="$(cd "$name" && git rev-parse HEAD)"
if [[ "$actual" == "$commit" ]]; then
  echo "OK: 체크아웃 커밋이 상위 포인터와 일치"
else
  echo "WARN: 체크아웃 커밋($actual)이 상위 포인터($commit)와 불일치"
  exit 2
fi

Length of output: 838


서브모듈 config-submodule의 원격 리포지토리 또는 커밋 미존재 확인 필요

git ls-remote https://github.com/ITZEEP/backend-config.git 실행 시 “Repository not found” 오류가 발생하여 커밋 2d91d5b967d71c3c6a3a658d296a1453141a3c24이 원격에 존재하지 않는 것으로 확인되었습니다. 아래 항목을 확인/수정해주세요:

  • .gitmodulessubmodule.config-submodule.url이 올바른 저장소를 가리키는지 검증
  • 해당 저장소 접근 권한(프라이빗 여부) 및 URL 정확성 확인
  • 커밋 SHA-1 해시가 실제로 원격에 존재하는지 확인
  • 필요 시 올바른 커밋 해시로 업데이트한 뒤 git submodule update --init 재실행 및 HEAD 일치 확인
🤖 Prompt for AI Agents
In config-submodule around lines 1-1, the submodule remote or commit is missing
on the remote (git ls-remote shows “Repository not found” for
https://github.com/ITZEEP/backend-config.git and commit 2d91d5b9… is absent);
verify and correct .gitmodules submodule.config-submodule.url to the proper
repository URL, confirm repository accessibility (private vs public) and
credentials, check that the referenced commit SHA exists on the remote (or
update to a valid SHA/branch), then update the submodule reference and run git
submodule sync && git submodule update --init --recursive and ensure the
submodule HEAD matches the expected commit.

Comment on lines +191 to +192
@RequestBody FinalContractDeletionResponseDto responseDto,
Authentication authentication);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

일관성 없는 메서드 파라미터 타입

FinalContractDeletionResponseDtoacceptFinalContractConfirmation 메서드의 파라미터로 사용되고 있는데, 메서드명과 DTO 이름이 일치하지 않습니다. 확정 수락용 별도 DTO를 만들거나 더 범용적인 DTO 이름을 사용하는 것이 좋습니다.

-    ResponseEntity<ApiResponse<Map<String, Object>>> acceptFinalContractConfirmation(
-            @PathVariable Long contractChatId,
-            @RequestBody FinalContractDeletionResponseDto responseDto,
-            Authentication authentication);
+    ResponseEntity<ApiResponse<Map<String, Object>>> acceptFinalContractConfirmation(
+            @PathVariable Long contractChatId,
+            @RequestBody FinalContractConfirmationResponseDto responseDto,
+            Authentication authentication);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@RequestBody FinalContractDeletionResponseDto responseDto,
Authentication authentication);
ResponseEntity<ApiResponse<Map<String, Object>>> acceptFinalContractConfirmation(
@PathVariable Long contractChatId,
@RequestBody FinalContractConfirmationResponseDto responseDto,
Authentication authentication);
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/controller/ContractChatController.java
around lines 191-192, the method acceptFinalContractConfirmation(...) currently
uses FinalContractDeletionResponseDto which does not match the method intent;
change this by either creating a dedicated DTO named
AcceptFinalContractRequestDto (or similar) for accepting the final contract and
replace the parameter type, or rename the existing DTO to a more
generic/appropriate name and update all usages; ensure the new DTO contains only
the required fields, update any validation, mappers, controller tests and API
docs/signatures accordingly so types are consistent with the method purpose.

Comment on lines +1 to +14
package org.scoula.domain.chat.dto;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FinalContractModificationRequestDto {
private Integer clauseOrder;
private String newTitle;
private String newContent;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

입력값 유효성 검증 애노테이션 추가 제안

외부 입력을 직접 받는 DTO입니다. 빈 문자열/음수 등 방어를 위해 Bean Validation을 추가하는 것이 안전합니다(컨트롤러 파라미터에 @Valid 병행 권장).

아래와 같이 보완을 제안드립니다:

 package org.scoula.domain.chat.dto;

 import lombok.*;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;

 @Getter
 @Setter
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
 public class FinalContractModificationRequestDto {
-      private Integer clauseOrder;
-      private String newTitle;
-      private String newContent;
+      @NotNull @Min(1)
+      private Integer clauseOrder;
+      @NotBlank
+      private String newTitle;
+      @NotBlank
+      private String newContent;
 }

컨트롤러 메서드 시그니처에 @Valid 적용 여부도 확인 부탁드립니다.


입력값 유효성 검증 설정 현황 확인을 위해 아래 스크립트를 실행해주세요:


🏁 Script executed:

#!/bin/bash
set -e

echo "1) @Valid 애노테이션 사용 여부 확인"
rg "@Valid" -n src/main/java || echo "  → @Valid 애노테이션 사용 내역이 없습니다."

echo
echo "2) Bean Validation 제약 애노테이션(import) 확인"
rg "jakarta.validation.constraints" -n src || rg "javax.validation.constraints" -n src || echo "  → 제약 애노테이션 import 내역이 없습니다."

echo
echo "3) 빌드 파일(validation 의존성) 확인"
if [ -f pom.xml ]; then
  echo "  • pom.xml:"
  rg "validation" -n pom.xml || echo "    → 의존성 항목 없음"
fi
if [ -f build.gradle ]; then
  echo "  • build.gradle:"
  rg "validation" -n build.gradle || echo "    → 의존성 항목 없음"
fi

Length of output: 8167


입력값 검증 애노테이션 추가 제안
FinalContractModificationRequestDto는 외부 입력을 받으므로 Bean Validation 제약 애노테이션을 추가하고, 이를 처리하는 컨트롤러에 @Valid가 적용되었는지 반드시 확인해주세요.

• 수정 대상

  • 파일: src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java

• Diff 예시

 package org.scoula.domain/chat/dto;

 import lombok.*;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;

 @Getter
 @Setter
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
 public class FinalContractModificationRequestDto {
-    private Integer clauseOrder;
-    private String newTitle;
-    private String newContent;
+    @NotNull
+    @Min(1)
+    private Integer clauseOrder;
+
+    @NotBlank
+    private String newTitle;
+
+    @NotBlank
+    private String newContent;
 }

• 확인 사항

  • 이 DTO를 받는 컨트롤러 메서드 파라미터에 @Valid @RequestBody FinalContractModificationRequestDto dto가 적용되어 있는지 검증
  • 이미 build.gradlejavax.validation:validation-api:2.0.1.Final 의존성이 포함되어 있음을 확인했습니다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
package org.scoula.domain.chat.dto;
import lombok.*;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FinalContractModificationRequestDto {
private Integer clauseOrder;
private String newTitle;
private String newContent;
}
package org.scoula.domain.chat.dto;
import lombok.*;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FinalContractModificationRequestDto {
@NotNull
@Min(1)
private Integer clauseOrder;
@NotBlank
private String newTitle;
@NotBlank
private String newContent;
}
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/dto/FinalContractModificationRequestDto.java
around lines 1-14, this DTO lacks Bean Validation annotations for external
input; add javax.validation constraints: annotate clauseOrder with @NotNull (and
@Min(1) if clause index must be positive) and annotate newTitle and newContent
with @NotBlank to prevent empty values, and ensure required imports are added.
Also verify every controller endpoint that accepts this DTO uses @Valid on the
@RequestBody parameter (e.g., @Valid @RequestBody
FinalContractModificationRequestDto dto) so validation is triggered, and update
any unit/integration tests or exception handlers if necessary to handle
ConstraintViolation/MethodArgumentNotValid exceptions.

Comment on lines +2120 to +2144
.build();

try {
String jsonData = objectMapper.writeValueAsString(requestData);
String valueData =
String.format(
"{\"clauseOrder\":%d,\"requestData\":%s}",
requestDto.getClauseOrder(), jsonData);
stringRedisTemplate.opsForValue().set(redisKey, valueData, Duration.ofHours(24));

String notificationMessage =
String.format(
"임대인이 특약 %d번 수정을 요청했습니다.\n\n" + "📝 수정 제목: %s\n" + "✏️ 수정 내용: %s\n\n",
requestDto.getClauseOrder(),
requestDto.getNewTitle(),
requestDto.getNewContent());

AiMessageBtn(contractChatId, notificationMessage);
log.info("수정 요청 Redis 저장 완료 - key: {}", redisKey);
return requestData;

} catch (Exception e) {
log.error("수정 요청 저장 실패", e);
throw new RuntimeException("수정 요청 저장 중 오류가 발생했습니다.");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

JSON 파싱 에러 처리 개선 필요

JSON 파싱 시 구체적인 예외 타입을 catch하고 더 명확한 에러 메시지를 제공해야 합니다.

-        } catch (Exception e) {
-            log.error("수정 요청 저장 실패", e);
-            throw new RuntimeException("수정 요청 저장 중 오류가 발생했습니다.");
-        }
+        } catch (JsonProcessingException e) {
+            log.error("JSON 직렬화 실패: {}", e.getMessage(), e);
+            throw new IllegalStateException("수정 요청 데이터 직렬화에 실패했습니다.", e);
+        } catch (Exception e) {
+            log.error("Redis 저장 실패: {}", e.getMessage(), e);
+            throw new RuntimeException("수정 요청 저장 중 예기치 않은 오류가 발생했습니다.", e);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.build();
try {
String jsonData = objectMapper.writeValueAsString(requestData);
String valueData =
String.format(
"{\"clauseOrder\":%d,\"requestData\":%s}",
requestDto.getClauseOrder(), jsonData);
stringRedisTemplate.opsForValue().set(redisKey, valueData, Duration.ofHours(24));
String notificationMessage =
String.format(
"임대인이 특약 %d번 수정을 요청했습니다.\n\n" + "📝 수정 제목: %s\n" + "✏️ 수정 내용: %s\n\n",
requestDto.getClauseOrder(),
requestDto.getNewTitle(),
requestDto.getNewContent());
AiMessageBtn(contractChatId, notificationMessage);
log.info("수정 요청 Redis 저장 완료 - key: {}", redisKey);
return requestData;
} catch (Exception e) {
log.error("수정 요청 저장 실패", e);
throw new RuntimeException("수정 요청 저장 중 오류가 발생했습니다.");
}
.build();
try {
String jsonData = objectMapper.writeValueAsString(requestData);
String valueData =
String.format(
"{\"clauseOrder\":%d,\"requestData\":%s}",
requestDto.getClauseOrder(), jsonData);
stringRedisTemplate.opsForValue().set(redisKey, valueData, Duration.ofHours(24));
String notificationMessage =
String.format(
"임대인이 특약 %d번 수정을 요청했습니다.\n\n" + "📝 수정 제목: %s\n" + "✏️ 수정 내용: %s\n\n",
requestDto.getClauseOrder(),
requestDto.getNewTitle(),
requestDto.getNewContent());
AiMessageBtn(contractChatId, notificationMessage);
log.info("수정 요청 Redis 저장 완료 - key: {}", redisKey);
return requestData;
} catch (JsonProcessingException e) {
log.error("JSON 직렬화 실패: {}", e.getMessage(), e);
throw new IllegalStateException("수정 요청 데이터 직렬화에 실패했습니다.", e);
} catch (Exception e) {
log.error("Redis 저장 실패: {}", e.getMessage(), e);
throw new RuntimeException("수정 요청 저장 중 예기치 않은 오류가 발생했습니다.", e);
}
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
around lines 2120 to 2144, replace the broad catch(Exception) with targeted
exception handling for JSON parsing and Redis errors: catch
com.fasterxml.jackson.core.JsonProcessingException to handle
objectMapper.writeValueAsString failures and log the exception message and
stacktrace, then throw a RuntimeException with a clear message and the original
exception as the cause; catch org.springframework.dao.DataAccessException (or
the specific Redis exception your template throws) around the redis set call to
log detailed redis error info and rethrow with the cause; keep a final
catch(Exception) only as a fallback that logs full details and rethrows the
exception as the cause so callers can inspect the original error.

Comment on lines 2549 to 2550
String param = getContractChatStatus(contractChatId.getStatus());
return "http://localhost:5173/contract/" + contractChatRoomId + param;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

하드코딩된 URL을 설정으로 분리하세요

프론트엔드 URL이 하드코딩되어 있습니다. 환경별로 다른 URL을 사용할 수 있도록 설정 파일로 분리해야 합니다.

+    @Value("${frontend.base-url}")
+    private String frontendBaseUrl;

-        return "http://localhost:5173/contract/" + contractChatRoomId + param;
+        return frontendBaseUrl + "/contract/" + contractChatRoomId + param;

application.yml에 추가:

frontend:
  base-url: ${FRONTEND_URL:http://localhost:5173}
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
around lines 2549 to 2550, the frontend base URL is hardcoded; change to read a
configurable property (e.g., frontend.base-url) injected into the service (via
@Value or a @ConfigurationProperties bean) and use that value to build the
returned URL, falling back to the existing default; add the suggested
application.yml entry (frontend.base-url: ${FRONTEND_URL:http://localhost:5173})
and update any tests to mock or provide the property as needed.

Comment on lines 422 to 434
ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
contractChatService.getContractChatStatus(contractChat.getStatus());
String contractChatUrls = "http://localhost:5173/contract/" + contractChatId.toString();
ChatMessageRequestDto linkMessages =
ChatMessageRequestDto.builder()
.chatRoomId(contractChatId)
.senderId(contractChat.getBuyerId())
.receiverId(contractChat.getOwnerId())
.content(contractChatUrls)
.type("URLLINK")
.build();
chatService.handleChatMessage(linkMessages);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

하드코딩 URL 제거, 불필요 호출 제거, 송수신자 반전 오류 수정, NPE 방지

  • 하드코딩된 프론트엔드 URL을 설정 기반으로 변경하세요.
  • getContractChatStatus(...) 호출 결과를 사용하지 않으므로 제거하세요.
  • 현재 senderId=buyer, receiverId=owner로 설정되어 있어, 임대인 플로우에서 송신/수신이 반대로 표기됩니다. owner가 발신자로 설정되어야 합니다.
  • contractChat 조회 실패 시 NPE를 방지하세요.

수정 제안 diff:

-        ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
-        contractChatService.getContractChatStatus(contractChat.getStatus());
-        String contractChatUrls = "http://localhost:5173/contract/" + contractChatId.toString();
+        ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
+        if (contractChat == null) {
+            throw new BusinessException(OwnerPreContractErrorCode.OWNER_SELECT, "계약 채팅 정보를 찾을 수 없습니다.");
+        }
+        String contractChatUrls = String.format("%s/contract/%d", frontendBaseUrl, contractChatId);
         ChatMessageRequestDto linkMessages =
                 ChatMessageRequestDto.builder()
                         .chatRoomId(contractChatId)
-                        .senderId(contractChat.getBuyerId())
-                        .receiverId(contractChat.getOwnerId())
+                        .senderId(contractChat.getOwnerId())
+                        .receiverId(contractChat.getBuyerId())
                         .content(contractChatUrls)
                         .type("URLLINK")
                         .build();
         chatService.handleChatMessage(linkMessages);

클래스 상단에 환경설정 주입 필드를 추가해 주세요.

import org.springframework.beans.factory.annotation.Value;
// ...
@Value("${app.frontend.base-url}")
private String frontendBaseUrl;
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java
around lines 422 to 434, remove the unused call to getContractChatStatus(...),
replace the hardcoded "http://localhost:5173" with a configurable frontend base
URL injected via @Value("${app.frontend.base-url}") (add the import and private
field at class top), guard against contractChat being null (check after
findByContractChatId and handle appropriately — throw a meaningful exception or
return early to avoid NPE), and fix the sender/receiver reversal by setting
senderId to contractChat.getOwnerId() and receiverId to
contractChat.getBuyerId() before building and sending the ChatMessageRequestDto.

Comment on lines 354 to 369
ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);

String contractChatUrls =
"http://localhost:5173/pre-contract/" + contractChatId.toString() + "/owner?step=1";
ChatMessageRequestDto linkMessages =
ChatMessageRequestDto.builder()
.chatRoomId(contractChatId)
.senderId(contractChat.getBuyerId())
.receiverId(contractChat.getOwnerId())
.content(contractChatUrls)
.type("URLLINK")
.build();
contractChatService.AiMessage(contractChatId, "안녕하세요!\n" + "임대인이 입장하면 바로 계약서 작성을 시작할게요.");
contractChatService.AiMessageBtn(contractChatId, "기다리는 동안 \n" + "어려운 법률 용어와 법률 팁을 알아볼까요?");
chatService.handleChatMessage(linkMessages);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

하드코딩된 프론트엔드 URL 제거 및 NPE 방지 처리

  • URL을 코드에 하드코딩하면 환경별(로컬/스테이징/운영) 배포시 오동작합니다. 환경변수/설정값 기반으로 구성하세요.
  • contractChatMapper.findByContractChatId의 반환값에 대한 NPE 방지가 필요합니다. 조회 실패 시 비즈니스 예외로 전환하는 편이 안전합니다.

아래와 같이 수정 제안드립니다.

-        ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
-
-        String contractChatUrls =
-                "http://localhost:5173/pre-contract/" + contractChatId.toString() + "/owner?step=1";
+        ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
+        if (contractChat == null) {
+            throw new BusinessException(PreContractErrorCode.TENANT_SELECT, "계약 채팅 정보를 찾을 수 없습니다.");
+        }
+
+        String contractChatUrls = String.format("%s/pre-contract/%d/owner?step=1", frontendBaseUrl, contractChatId);
         ChatMessageRequestDto linkMessages =
                 ChatMessageRequestDto.builder()
                         .chatRoomId(contractChatId)
                         .senderId(contractChat.getBuyerId())
                         .receiverId(contractChat.getOwnerId())
                         .content(contractChatUrls)
                         .type("URLLINK")
                         .build();
         contractChatService.AiMessage(contractChatId, "안녕하세요!\n" + "임대인이 입장하면 바로 계약서 작성을 시작할게요.");
         contractChatService.AiMessageBtn(contractChatId, "기다리는 동안 \n" + "어려운 법률 용어와 법률 팁을 알아볼까요?");
         chatService.handleChatMessage(linkMessages);

클래스 상단에 환경설정 주입 필드를 추가해 주세요.

import org.springframework.beans.factory.annotation.Value;
// ...
@Value("${app.frontend.base-url}")
private String frontendBaseUrl;

추가 확인:

  • 메시지 타입 "URLLINK"는 시스템 전반에서 합의된 상수인가요? Enum 전환 또는 상수화가 가능하면 더 안전합니다.

추가 검증을 위해 코드베이스를 살펴봅니다.


🏁 Script executed:

#!/bin/bash
set -e

echo "1) findByContractChatId 사용 위치:"
rg "findByContractChatId" -n src

echo -e "\n2) ContractChatMapper 인터페이스 정의:"
rg "interface ContractChatMapper" -n src -A10

echo -e "\n3) \"URLLINK\" 문자열 사용 위치:"
rg "\"URLLINK\"" -n src

echo -e "\n4) ChatMessageRequestDto 클래스 정의:"
rg "class ChatMessageRequestDto" -n src -A30

echo -e "\n5) 프로젝트 내 Enum 정의 목록:"
rg "enum" -n src -n

Length of output: 24494


하드코딩된 프론트엔드 URL 제거 및 null 처리 추가

환경별(front/스테이징/운영) URL을 application.yml 등 설정 파일로 분리하고, findByContractChatId 호출 결과에 대한 NPE 방지를 위해 null 체크를 반드시 추가해주세요. 또한 메시지 타입 "URLLINK"는 상수나 enum으로 관리하는 것이 안전합니다.

수정 대상:

  • src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java (354–369행)
  • src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java (유사 로직)
  • application.yml 또는 각 환경별 프로퍼티 파일

예시 diff:

--- a/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java
+++ b/src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java
@@ -10,6 +10,8 @@
 import org.scoula.domain.precontract.enums.RentType;
 import org.springframework.beans.factory.annotation.Value;
 
+@Value("${app.frontend.base-url}")
+private String frontendBaseUrl;
 
 public class PreContractServiceImpl implements PreContractService {
@@ -352,11 +354,17 @@
         // 1) ContractChat 조회 및 null 체크
-        ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
+        ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
+        if (contractChat == null) {
+            throw new BusinessException(
+                PreContractErrorCode.CONTRACT_CHAT_NOT_FOUND, 
+                "해당 계약 채팅 정보를 찾을 수 없습니다. contractChatId=" + contractChatId
+            );
+        }
 
-        String contractChatUrls =
-                "http://localhost:5173/pre-contract/" + contractChatId + "/owner?step=1";
+        // 2) URL 환경변수화
+        String contractChatUrls = String.format(
+            "%s/pre-contract/%d/owner?step=1", frontendBaseUrl, contractChatId
+        );
 
         ChatMessageRequestDto linkMessages = ChatMessageRequestDto.builder()
             .chatRoomId(contractChatId)
@@ -364,7 +372,7 @@
             .content(contractChatUrls)
-            .type("URLLINK")
+            .type(ChatMessageType.URL_LINK)   // enum 또는 상수로 변경
             .build();
 
         contractChatService.AiMessage(…

application.yml 예시:

app:
  frontend:
    base-url: https://your-frontend-domain.com

추가 제안:

  • PreContractErrorCodeCONTRACT_CHAT_NOT_FOUND 같은 전용 에러 코드를 추가해주시고,
  • "URLLINK" 대신 ChatMessageType enum 혹은 ChatConstants.URL_LINK 상수를 만들어 사용해주세요.
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java
around lines 354 to 369, the code hardcodes the frontend URL, does not
null-check the result of contractChatMapper.findByContractChatId(...), and uses
a raw string for the message type; update to read the frontend base URL from
application.yml (e.g. app.frontend.base-url), add a null check after
findByContractChatId and throw/use a
PreContractErrorCode.CONTRACT_CHAT_NOT_FOUND (add that enum value if missing)
when null, construct the link using the injected config value, and replace the
literal "URLLINK" with a ChatMessageType enum or ChatConstants.URL_LINK; apply
the same changes to OwnerPreContractServiceImpl and add the
app.frontend.base-url entry to application.yml for each environment.

@MeongW MeongW merged commit c6b1739 into develop Aug 13, 2025
4 of 6 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (7)
src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (2)

110-123: URLLINK 프리뷰 처리 추가, 잘 반영됨

"TEXT/URLLINK/FILE"에 대한 미리보기 문구가 명확해져 UX 측면에서 개선되었습니다. 기존 리뷰 지적사항을 정확히 반영했습니다.


204-230: URLLINK 알림 케이스 누락 → switch에 케이스 추가 필요

계약 수락 후 발송되는 링크 메시지(type="URLLINK", Line 850)와 정합성을 맞추려면 알림 switch에도 "URLLINK" 케이스를 추가해야 합니다. 현재는 default로 처리되어 제목/본문이 의도와 다를 수 있습니다.

아래 diff처럼 케이스를 보강해 주세요.

                       switch (dto.getType()) {
                           case "TEXT":
                               notificationBody = dto.getContent();
                               notificationTitle = senderName + "님의 새 메시지";
                               break;
+                          case "URLLINK":
+                              notificationBody = "[링크] " + (dto.getContent() != null ? dto.getContent() : "");
+                              notificationTitle = senderName + "님이 링크를 보냈습니다";
+                              break;
                           case "FILE":
                               notificationBody =
                                       "[파일] "
                                               + (dto.getContent() != null
                                                       ? dto.getContent()
                                                       : "파일을 보냈습니다");
                               notificationTitle = senderName + "님이 파일을 보냈습니다";
                               break;
                           case "CONTRACT_REQUEST":
                               notificationBody = "계약을 요청했습니다";
                               notificationTitle = senderName + "님의 계약 요청";
                               break;
                           case "CONTRACT_REJECT":
                               notificationBody = "계약 요청을 거절했습니다";
                               notificationTitle = senderName + "님의 계약 거절";
                               break;
                           default:
                               notificationBody =
                                       dto.getContent() != null ? dto.getContent() : "새 메시지";
                               notificationTitle = senderName + "님의 새 메시지";
                               break;
                       }

Also applies to: 844-851

src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (3)

50-50: ObjectMapper를 Spring Bean으로 주입받아 사용하세요

ObjectMapper를 직접 생성하지 말고 Spring이 관리하는 Bean으로 주입받아 사용하는 것이 좋습니다. 이렇게 하면 전역 설정을 일관되게 적용할 수 있습니다.

-    private final ObjectMapper objectMapper = new ObjectMapper();
+    private final ObjectMapper objectMapper;

생성자에 ObjectMapper를 추가하면 Spring이 자동으로 주입합니다 (@requiredargsconstructor 사용 중).


1063-1088: 중복 코드를 별도 메서드로 추출하세요

최종 특약서 저장 및 상태 업데이트 로직이 중복됩니다. 별도 메서드로 추출하면 유지보수성이 향상됩니다.

+    private Map<String, Object> processFinalContractCompletion(Long contractChatId, String message) {
+        try {
+            FinalSpecialContractDocument finalContract = saveFinalSpecialContract(contractChatId);
+            AiMessageNext(contractChatId, message);
+            contractChatMapper.updateStatus(contractChatId, ContractChat.ContractStatus.ROUND4);
+            
+            return Map.of(
+                "message", "모든 특약이 완료되었습니다!",
+                "completed", true,
+                "finalContractId", finalContract.getId(),
+                "totalFinalClauses", finalContract.getTotalFinalClauses()
+            );
+        } catch (Exception e) {
+            log.error("최종 특약 저장 실패", e);
+            return Map.of("message", "특약은 완료되었지만 최종 저장 중 오류가 발생했습니다.", "completed", true);
+        }
+    }

                  if (remainingIncompleteContracts.isEmpty()) {
-                      try {
-                          FinalSpecialContractDocument finalContract =
-                                  saveFinalSpecialContract(contractChatId);
-
-                          AiMessageNext(contractChatId, "🎉 모든 특약 협상이 완료되었습니다! 최종 특약서가 생성되었습니다.");
-                          contractChatMapper.updateStatus(
-                                  contractChatId, ContractChat.ContractStatus.ROUND4);
-
-                          return Map.of(
-                                  "message",
-                                  "모든 특약이 완료되었습니다!",
-                                  "completed",
-                                  true,
-                                  "finalContractId",
-                                  finalContract.getId(),
-                                  "totalFinalClauses",
-                                  finalContract.getTotalFinalClauses());
-                      } catch (Exception e) {
-                          log.error("최종 특약 저장 실패", e);
-                          return Map.of(
-                                  "message", "특약은 완료되었지만 최종 저장 중 오류가 발생했습니다.", "completed", true);
-                      }
+                      return processFinalContractCompletion(contractChatId, 
+                          "🎉 모든 특약 협상이 완료되었습니다! 최종 특약서가 생성되었습니다.");
                  }

2126-2150: JSON 파싱 에러 처리를 개선하세요

JSON 파싱 시 구체적인 예외 타입을 catch하고 더 명확한 에러 메시지를 제공해야 합니다.

-        } catch (Exception e) {
-            log.error("수정 요청 저장 실패", e);
-            throw new RuntimeException("수정 요청 저장 중 오류가 발생했습니다.");
-        }
+        } catch (JsonProcessingException e) {
+            log.error("JSON 직렬화 실패: {}", e.getMessage(), e);
+            throw new IllegalStateException("수정 요청 데이터 직렬화에 실패했습니다.", e);
+        } catch (Exception e) {
+            log.error("Redis 저장 실패: {}", e.getMessage(), e);
+            throw new RuntimeException("수정 요청 저장 중 예기치 않은 오류가 발생했습니다.", e);
+        }
src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java (1)

361-376: 프론트엔드 URL 하드코딩 제거 및 null 체크 추가

환경별(로컬/스테이징/운영) URL 설정이 필요하고, contractChat이 null일 경우 NPE가 발생할 수 있습니다.

     ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
+    if (contractChat == null) {
+        throw new BusinessException(PreContractErrorCode.TENANT_SELECT, 
+            "계약 채팅 정보를 찾을 수 없습니다: contractChatId=" + contractChatId);
+    }

     String contractChatUrls = URL + precontractUrl + (contractChatId.toString()) + ownerUrl;

또한 메시지 타입 "URLLINK"를 상수나 enum으로 관리하는 것을 권장합니다:

-                        .type("URLLINK")
+                        .type(ChatMessageType.URL_LINK)  // 또는 ChatConstants.URL_LINK
src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java (1)

427-444: 불필요한 메서드 호출 제거 및 null 체크 추가

  1. getContractChatStatus() 호출 결과를 사용하지 않으므로 제거해야 합니다
  2. contractChatchatRoom에 대한 null 체크가 필요합니다
  3. 송수신자가 반대로 설정되어 있습니다 (임대인 플로우에서는 owner가 발신자여야 함)
     ContractChat contractChat = contractChatMapper.findByContractChatId(contractChatId);
+    if (contractChat == null) {
+        throw new BusinessException(OwnerPreContractErrorCode.OWNER_SELECT, 
+            "계약 채팅 정보를 찾을 수 없습니다: contractChatId=" + contractChatId);
+    }
+    
     ChatRoom chatRoom =
             chatRoomMapper.findByUserAndHome(
                     contractChat.getOwnerId(),
                     contractChat.getBuyerId(),
                     contractChat.getHomeId());
-    contractChatService.getContractChatStatus(contractChat.getStatus());
+    if (chatRoom == null) {
+        throw new BusinessException(OwnerPreContractErrorCode.OWNER_SELECT, 
+            "채팅방을 찾을 수 없습니다");
+    }
+    
     ChatMessageRequestDto linkMessages =
             ChatMessageRequestDto.builder()
                     .chatRoomId(chatRoom.getChatRoomId())
-                    .senderId(contractChat.getBuyerId())
-                    .receiverId(contractChat.getOwnerId())
+                    .senderId(contractChat.getOwnerId())
+                    .receiverId(contractChat.getBuyerId())
                     .content("계약 채팅방 URL")
                     .type("URLLINK")
                     .build();
     chatService.handleChatMessage(linkMessages);
-    contractChatService.AiMessage(contractChatId, "임대인꼐서 입장하셨습니다! 🤝 이제 계약을 시작합니다.");
+    contractChatService.AiMessage(contractChatId, "임대인께서 입장하셨습니다! 🤝 이제 계약을 시작합니다.");
🧹 Nitpick comments (7)
src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (2)

56-61: 환경 URL 필드명과 경로 상수 선언 방식 개선

  • 인스턴스 필드명을 대문자(URL)로 두면 타입(java.net.URL)과 혼동될 수 있고 관례에도 어긋납니다. 의미가 드러나도록 frontBaseUrl 등으로 변경 권장.
  • 경로 상수는 불변(static final)로 선언하고 상수 네이밍 컨벤션(UPPER_SNAKE_CASE)을 따르는 것을 권장합니다.

아래처럼 정리하면 가독성과 안전성이 올라갑니다.

-      @Value("${front.base.url}")
-      private String URL;
-
-      private String PRECONTRACTURL = "/pre-contract/";
-      private String BUYERURL = "/buyer?step=1";
+      @Value("${front.base.url}")
+      private String frontBaseUrl;
+
+      private static final String PRECONTRACT_PATH = "/pre-contract/";
+      private static final String BUYER_PATH = "/buyer?step=1";

842-842: 계약 채팅 URL 조립 시 이중 슬래시/인코딩 이슈 예방

베이스 URL이 슬래시로 끝나는 경우 등 문자열 단순 연결은 이중 슬래시가 생길 수 있습니다. UriComponentsBuilder로 조립하면 안전합니다. (Line 56–61에서 제안한 네이밍을 적용한 예시입니다.)

-          String contractChatUrl = URL + PRECONTRACTURL + (contractChatRoomId.toString()) + BUYERURL;
+          String contractChatUrl = UriComponentsBuilder.fromHttpUrl(frontBaseUrl)
+                  .pathSegment("pre-contract", contractChatRoomId.toString(), "buyer")
+                  .queryParam("step", 1)
+                  .build()
+                  .toUriString();

아래 import 추가 필요:

import org.springframework.web.util.UriComponentsBuilder;

참고: 만약 필드명을 그대로 유지한다면 frontBaseUrl → URL로 변경해 사용하셔도 됩니다.

src/main/java/org/scoula/domain/chat/vo/ContractChat.java (2)

40-53: ROUND4에 대한 라운드 매핑 추가 LGTM

ROUND4 -> 5L 매핑 보완으로 이전 리뷰에서 지적된 누락이 해결되었습니다. 동작 측면 문제 없습니다.

중복 switch/조건을 줄이기 위해 enum에 라운드 인덱스를 속성으로 부여하면 가독성이 좋아집니다.

예시:

public enum ContractStatus {
  STEP0(null), STEP1(null), STEP2(null),
  ROUND0(1L), ROUND1(2L), ROUND2(3L), ROUND3(4L), ROUND4(5L),
  STEP4(null);

  private final Long roundIndex;
  ContractStatus(Long roundIndex) { this.roundIndex = roundIndex; }
  public boolean isRound() { return roundIndex != null; }
  public Long getRoundIndex() { return roundIndex; }
}

// 사용부
public Long getCurrentRound() {
  return (status != null && status.getRoundIndex() != null) ? status.getRoundIndex() : 1L;
}

56-64: ROUND4에 대한 라운드 여부 판정 추가 LGTM

라운드 포함 여부에 ROUND4가 반영되었습니다. 추후 유지보수를 위해 enum에 isRound() 유틸을 두는 방식을 권장합니다(위 코멘트 참조).

src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml (1)

14-19: 쿼리 추가 OK. 포맷팅/스타일 컨벤션 일치 권장

기능은 문제없습니다. 다른 쿼리들과 동일한 들여쓰기/공백 컨벤션을 맞추면 가독성이 좋아집니다.

적용 예시(diff):

-    <select id="selectContractOwnerId" resultType="Long">
-    SELECT owner_id
-    FROM contract_chat
-    WHERE contract_chat_id=#{contractChatId}
-</select>
+    <select id="selectContractOwnerId" resultType="Long">
+        SELECT owner_id
+        FROM contract_chat
+        WHERE contract_chat_id = #{contractChatId}
+    </select>
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (1)

476-482: 검증 메서드 일관성/의미 명확화 제안

  • 예외 코드: 소유자 검증 실패에 PreContractErrorCode.TENANT_USER를 재사용하면 의미가 모호합니다. OWNER_USER 또는 UNAUTHORIZED 성격의 코드 신설/사용을 권장합니다.
  • 중복 제거: validateUserId(=buyer), validateIsOwner(=owner)를 내부 공통 메서드로 일반화하면 유지보수가 수월합니다.

예시:

private void validateParticipant(Long contractChatId, Long userId, Function<Long, Optional<Long>> idResolver, BusinessException whenNotMatch) {
  Long expected = idResolver.apply(contractChatId).orElseThrow(() -> whenNotMatch);
  if (!userId.equals(expected)) throw whenNotMatch;
}
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java (1)

76-83: 중복 제거 OK. 헬퍼 사용으로 인증 로직 간결화됨

getUserIdFromAuthentication 사용으로 기존 중복 코드가 정리되었습니다. 추가로 전 엔드포인트에 Swagger 문서화(@operation 등) 적용을 권장합니다.

예시:

@Operation(summary = "계약 채팅 URL 이동", description = "계약 채팅방 URL을 반환합니다.")
@GetMapping("/{chatRoomId}/moveContractChat")
public ResponseEntity<ApiResponse<String>> moveContractChat(...)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa09a0f and c025732.

📒 Files selected for processing (10)
  • config-submodule (1 hunks)
  • src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java (6 hunks)
  • src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (4 hunks)
  • src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (12 hunks)
  • src/main/java/org/scoula/domain/chat/vo/ContractChat.java (3 hunks)
  • src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (2 hunks)
  • src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java (1 hunks)
  • src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java (4 hunks)
  • src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java (4 hunks)
  • src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • config-submodule
🧰 Additional context used
📓 Path-based instructions (5)
src/main/java/org/scoula/**/service/*Service{Interface,Impl}.java

📄 CodeRabbit Inference Engine (CLAUDE.md)

Use interface-implementation pattern for services: *ServiceInterface.java (interface) and *ServiceImpl.java (implementation)

Files:

  • src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
  • src/main/java/org/scoula/domain/precontract/service/PreContractServiceImpl.java
  • src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java
  • src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java
  • src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
src/main/java/org/scoula/domain/*/mapper/*Mapper.java

📄 CodeRabbit Inference Engine (CLAUDE.md)

src/main/java/org/scoula/domain/*/mapper/*Mapper.java: Define MyBatis mapper interfaces under src/main/java/org/scoula/domain/*/mapper/*Mapper.java
Annotate MyBatis mapper interfaces with @Mapper

Files:

  • src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java
src/main/resources/org/scoula/domain/*/mapper/*Mapper.xml

📄 CodeRabbit Inference Engine (CLAUDE.md)

src/main/resources/org/scoula/domain/*/mapper/*Mapper.xml: Place MyBatis XML mappers at src/main/resources/org/scoula/domain/*/mapper/*Mapper.xml
Name MyBatis XML mappers *Mapper.xml to match their corresponding *Mapper.java interfaces

Files:

  • src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml
src/main/java/org/scoula/**/controller/*Controller{,Impl}.java

📄 CodeRabbit Inference Engine (CLAUDE.md)

Use interface-implementation pattern for controllers: *Controller.java (interface) and *ControllerImpl.java (implementation)

Files:

  • src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
src/main/java/org/scoula/**/controller/**/*.java

📄 CodeRabbit Inference Engine (CLAUDE.md)

src/main/java/org/scoula/**/controller/**/*.java: All controller endpoints should return a consistent ApiResponse<T> wrapper
Add Swagger annotations to controller endpoints for API documentation

Files:

  • src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
🧠 Learnings (3)
📚 Learning: 2025-08-04T00:51:44.587Z
Learnt from: Whatdoyumin
PR: ITZEEP/backend#40
File: src/main/resources/org/scoula/domain/precontract/mapper/OwnerPreContractMapper.xml:188-192
Timestamp: 2025-08-04T00:51:44.587Z
Learning: In the ITZEEP backend codebase, the owner_precontract_check table uses contract_chat_id as both primary key (PK) and foreign key (FK), so selectOwnerPrecheckId correctly returns contract_chat_id for use in upsertRestoreScope operations. This is confirmed by the join pattern "LEFT JOIN owner_restore_scope ors ON opc.contract_chat_id = ors.owner_precheck_id" in the mapper files.

Applied to files:

  • src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java
  • src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml
📚 Learning: 2025-08-04T00:51:44.587Z
Learnt from: Whatdoyumin
PR: ITZEEP/backend#40
File: src/main/resources/org/scoula/domain/precontract/mapper/OwnerPreContractMapper.xml:188-192
Timestamp: 2025-08-04T00:51:44.587Z
Learning: In the ITZEEP backend codebase, the owner_precontract_check table uses contract_chat_id as both primary key (PK) and foreign key (FK), so selectOwnerPrecheckId correctly returns contract_chat_id for use in upsertRestoreScope operations.

Applied to files:

  • src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java
  • src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml
📚 Learning: 2025-07-30T11:26:48.397Z
Learnt from: minnieming
PR: ITZEEP/backend#29
File: src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml:0-0
Timestamp: 2025-07-30T11:26:48.397Z
Learning: In the ITZEEP backend codebase's precontract domain, the developer prefers to separate MyBatis queries when they have different return types (e.g., Long vs String) rather than combining them into a single query with a resultMap, for clarity and maintainability purposes.

Applied to files:

  • src/main/resources/org/scoula/domain/precontract/mapper/TenantPreContractMapper.xml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: CodeQL Security Analysis (java)
  • GitHub Check: Test Coverage
🔇 Additional comments (10)
src/main/java/org/scoula/domain/chat/service/ChatServiceImpl.java (2)

844-851: 계약 수락 후 링크 메시지: senderId=소유자, type=URLLINK 전환 적절

수락 후 링크 전달 주체를 소유자로, 타입을 URLLINK로 보내도록 변경한 판단이 명확합니다. 프론트 미리보기/알림 정책과도 일관성이 높아집니다(알림 switch의 URLLINK 케이스만 보강하면 완전).


655-671: 검토 결과: ChatServiceImpl.isUserInContractChatRoom 로직은 문제 없습니다
ChatControllerImpl의 enterChatRoom 핸들러가 전달받은 chatRoomId 파라미터에 계약 채팅방 ID를 그대로 사용하여 setUserCurrentChatRoom(userId, chatRoomId) 를 호출하므로, userCurrentChatRoom 맵을 통해 일반/계약 채팅방을 모두 올바르게 추적합니다.
또한 계약 채팅 전용으로는 ContractChatServiceImpl 에서 별도의 맵(contractChatOnlineUsers)과 메서드(isUserInContractChatRoom)를 두고 있어, 전용 맵 추가나 분리된 훅 구현은 필요치 않습니다.

Likely an incorrect or invalid review comment.

src/main/java/org/scoula/domain/chat/vo/ContractChat.java (1)

32-34: ROUND4/STEP4 추가: ordinal 의존성 확인 결과

  • JPA(@Enumerated) 매핑 어노테이션이나 EnumType.ORDINAL 사용 흔적이 없습니다.
  • MyBatis XML 매퍼(src/main/resources/org/scoula/domain/chat/mapper/ContractChatMapper.xml)에서 resultType="…ContractChat$ContractStatus" 으로만 매핑되어 있어, 기본 EnumTypeHandler(문자열 기반)로 동작합니다.

하지만 DB 스키마의 contract_chat.status 컬럼이 VARCHAR(…)로 정의되어 있는지, 별도 커스텀 TypeHandler가 등록되어 있지 않은지 직접 한번 더 확인해 주세요.

src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (1)

85-86: Mongo 계약서 최초 생성 권한을 ‘소유자’로 제한 — 요구사항 확인 권장

saveContractMongo에서 buyer 검증 → owner 검증으로 변경되었습니다. 제품 요구사항에 부합하는지(초기 생성 권한이 임대인 한정인지) 프론트/기획과 합의된 내용인지 확인해 주세요.

변경 영향:

  • 기존에 임차인이 계약서를 생성하던 플로우가 있었다면 403/에러로 바뀝니다.
  • 관련 E2E/통합 테스트 업데이트 필요.
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java (4)

497-497: 사소: @OverRide 추가 변경

별도 이슈 없습니다.


549-579: 접근 제어 포함 및 흐름 적절

사용자 인증 → 접근권한 체크 → 업데이트 → 시작점 세팅의 플로우가 명확합니다.


893-939: 최종 특약 확정 응답: 역할 검증 및 Redis 키 체크 적절

임차인 역할 검증 및 Redis 키 검증 플로우가 명확합니다. 응답 메시지도 일관적입니다.


941-964: 계약 상태 조회: 접근권한 체크 및 상태 유효성 검사 적절

ApiResponse 래핑과 에러 메시지 일관성도 좋습니다.

src/main/java/org/scoula/domain/precontract/service/OwnerPreContractServiceImpl.java (2)

605-609: 빌더 패턴 반환 개선

AI 특약 요청을 위한 빌더가 이제 null 대신 적절한 객체를 반환하도록 개선되었습니다.


614-616: null 체크 로직 개선됨

contractDocument가 null일 때 빈 OcrData 대신 null을 반환하도록 개선되어 더 명확한 의도를 전달합니다.

Comment on lines +666 to +684
@Override
@GetMapping("/special-contract/{contractChatId}/incomplete/now")
public ResponseEntity<ApiResponse<List<SpecialContractFixDocument>>>
getIncompleteSpecialContractsWithoutMessage(
@PathVariable Long contractChatId, Authentication authentication) {

try {
Long userId = getUserIdFromAuthentication(authentication);
List<SpecialContractFixDocument> result =
contractChatService.getIncompleteSpecialContractsWithoutMessage(
contractChatId, userId);

return ResponseEntity.ok(ApiResponse.success(result, "메시지가 없는 미완료 특약 목록 조회 성공"));
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(ApiResponse.error("INTERNAL_ERROR", "미완료 특약 목록 조회 중 오류가 발생했습니다."));
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

접근권한 체크 누락 — 임의 접근 가능성

getIncompleteSpecialContractsWithoutMessage는 isUserInContractChat 체크가 없습니다. 다른 엔드포인트와 일관되게 접근 제어를 추가하세요.

수정안(diff):

   public ResponseEntity<ApiResponse<List<SpecialContractFixDocument>>>
           getIncompleteSpecialContractsWithoutMessage(
                   @PathVariable Long contractChatId, Authentication authentication) {

       try {
           Long userId = getUserIdFromAuthentication(authentication);
+          if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+              return ResponseEntity.badRequest()
+                  .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+          }
           List<SpecialContractFixDocument> result =
                   contractChatService.getIncompleteSpecialContractsWithoutMessage(
                           contractChatId, userId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
@GetMapping("/special-contract/{contractChatId}/incomplete/now")
public ResponseEntity<ApiResponse<List<SpecialContractFixDocument>>>
getIncompleteSpecialContractsWithoutMessage(
@PathVariable Long contractChatId, Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
List<SpecialContractFixDocument> result =
contractChatService.getIncompleteSpecialContractsWithoutMessage(
contractChatId, userId);
return ResponseEntity.ok(ApiResponse.success(result, "메시지가 없는 미완료 특약 목록 조회 성공"));
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(ApiResponse.error("INTERNAL_ERROR", "미완료 특약 목록 조회 중 오류가 발생했습니다."));
}
}
@Override
@GetMapping("/special-contract/{contractChatId}/incomplete/now")
public ResponseEntity<ApiResponse<List<SpecialContractFixDocument>>>
getIncompleteSpecialContractsWithoutMessage(
@PathVariable Long contractChatId, Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
return ResponseEntity.badRequest()
.body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
}
List<SpecialContractFixDocument> result =
contractChatService.getIncompleteSpecialContractsWithoutMessage(
contractChatId, userId);
return ResponseEntity.ok(ApiResponse.success(result, "메시지가 없는 미완료 특약 목록 조회 성공"));
} catch (Exception e) {
return ResponseEntity.internalServerError()
.body(ApiResponse.error("INTERNAL_ERROR", "미완료 특약 목록 조회 중 오류가 발생했습니다."));
}
}
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
around lines 666 to 684, the method getIncompleteSpecialContractsWithoutMessage
is missing the access control check present in other endpoints; before calling
contractChatService.getIncompleteSpecialContractsWithoutMessage, retrieve the
userId from Authentication, call
contractChatService.isUserInContractChat(contractChatId, userId) and if it
returns false immediately return a 403 ResponseEntity with an ApiResponse.error
code (e.g., "FORBIDDEN", message "접근 권한이 없습니다."), otherwise continue to call the
service and return the success response as before; keep the existing try/catch
handling.

Comment on lines +724 to +747
@Override
@PostMapping("/final-contract/{contractChatId}/modification-request")
public ResponseEntity<ApiResponse<ModificationRequestData>> requestFinalContractModification(
@PathVariable Long contractChatId,
@RequestBody FinalContractModificationRequestDto requestDto,
Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);

ModificationRequestData result =
contractChatService.requestFinalContractModification(
contractChatId, userId, requestDto);

return ResponseEntity.ok(ApiResponse.success(result, "수정 요청이 성공적으로 전송되었습니다."));

} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponse.error("REQUEST_FAILED", e.getMessage()));
} catch (Exception e) {
log.error("최종 특약서 수정 요청 실패", e);
return ResponseEntity.internalServerError()
.body(ApiResponse.error("INTERNAL_ERROR", "수정 요청 중 오류가 발생했습니다."));
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

최종 특약 수정 요청: 접근권한 체크 누락

해당 계약 채팅 참여자 검증이 없습니다. 일관된 보안을 위해 추가 필요합니다.

수정안(diff):

   public ResponseEntity<ApiResponse<ModificationRequestData>> requestFinalContractModification(
           @PathVariable Long contractChatId,
           @RequestBody FinalContractModificationRequestDto requestDto,
           Authentication authentication) {
       try {
           Long userId = getUserIdFromAuthentication(authentication);
+          if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+              return ResponseEntity.badRequest()
+                  .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+          }

           ModificationRequestData result =
                   contractChatService.requestFinalContractModification(
                           contractChatId, userId, requestDto);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
@PostMapping("/final-contract/{contractChatId}/modification-request")
public ResponseEntity<ApiResponse<ModificationRequestData>> requestFinalContractModification(
@PathVariable Long contractChatId,
@RequestBody FinalContractModificationRequestDto requestDto,
Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
ModificationRequestData result =
contractChatService.requestFinalContractModification(
contractChatId, userId, requestDto);
return ResponseEntity.ok(ApiResponse.success(result, "수정 요청이 성공적으로 전송되었습니다."));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponse.error("REQUEST_FAILED", e.getMessage()));
} catch (Exception e) {
log.error("최종 특약서 수정 요청 실패", e);
return ResponseEntity.internalServerError()
.body(ApiResponse.error("INTERNAL_ERROR", "수정 요청 중 오류가 발생했습니다."));
}
}
@Override
@PostMapping("/final-contract/{contractChatId}/modification-request")
public ResponseEntity<ApiResponse<ModificationRequestData>> requestFinalContractModification(
@PathVariable Long contractChatId,
@RequestBody FinalContractModificationRequestDto requestDto,
Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
+ if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+ return ResponseEntity.badRequest()
+ .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+ }
ModificationRequestData result =
contractChatService.requestFinalContractModification(
contractChatId, userId, requestDto);
return ResponseEntity.ok(ApiResponse.success(result, "수정 요청이 성공적으로 전송되었습니다."));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponse.error("REQUEST_FAILED", e.getMessage()));
} catch (Exception e) {
log.error("최종 특약서 수정 요청 실패", e);
return ResponseEntity.internalServerError()
.body(ApiResponse.error("INTERNAL_ERROR", "수정 요청 중 오류가 발생했습니다."));
}
}
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
around lines 724 to 747, the handler for POST
/final-contract/{contractChatId}/modification-request is missing a check that
the authenticated user is a participant of the contract chat; add a validation
after retrieving userId that calls a service method (e.g.
contractChatService.isParticipant(contractChatId, userId) or
contractChatService.validateParticipant(contractChatId, userId)) and if the
check fails return
ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiResponse.error("FORBIDDEN",
"권한이 없습니다.")) before calling requestFinalContractModification so only
participants can send modification requests.

Comment on lines +749 to +774
@Override
@PostMapping("/final-contract/{contractChatId}/modification-response")
public ResponseEntity<ApiResponse<FinalSpecialContractDocument>> respondToModificationRequest(
@PathVariable Long contractChatId,
@RequestBody FinalContractModificationResponseDto responseDto,
Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);

FinalSpecialContractDocument result =
contractChatService.respondToModificationRequest(
contractChatId, userId, responseDto);

String message = responseDto.isAccepted() ? "수정 요청을 수락했습니다." : "수정 요청을 거절했습니다.";

return ResponseEntity.ok(ApiResponse.success(result, message));

} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponse.error("RESPONSE_FAILED", e.getMessage()));
} catch (Exception e) {
log.error("수정 요청 응답 실패", e);
return ResponseEntity.internalServerError()
.body(ApiResponse.error("INTERNAL_ERROR", "응답 처리 중 오류가 발생했습니다."));
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

최종 특약 수정 응답: 접근권한 체크 누락

요청과 동일하게 접근 제어 추가가 필요합니다.

수정안(diff):

   public ResponseEntity<ApiResponse<FinalSpecialContractDocument>> respondToModificationRequest(
           @PathVariable Long contractChatId,
           @RequestBody FinalContractModificationResponseDto responseDto,
           Authentication authentication) {
       try {
           Long userId = getUserIdFromAuthentication(authentication);
+          if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+              return ResponseEntity.badRequest()
+                  .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+          }

           FinalSpecialContractDocument result =
                   contractChatService.respondToModificationRequest(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
@PostMapping("/final-contract/{contractChatId}/modification-response")
public ResponseEntity<ApiResponse<FinalSpecialContractDocument>> respondToModificationRequest(
@PathVariable Long contractChatId,
@RequestBody FinalContractModificationResponseDto responseDto,
Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
FinalSpecialContractDocument result =
contractChatService.respondToModificationRequest(
contractChatId, userId, responseDto);
String message = responseDto.isAccepted() ? "수정 요청을 수락했습니다." : "수정 요청을 거절했습니다.";
return ResponseEntity.ok(ApiResponse.success(result, message));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponse.error("RESPONSE_FAILED", e.getMessage()));
} catch (Exception e) {
log.error("수정 요청 응답 실패", e);
return ResponseEntity.internalServerError()
.body(ApiResponse.error("INTERNAL_ERROR", "응답 처리 중 오류가 발생했습니다."));
}
}
@Override
@PostMapping("/final-contract/{contractChatId}/modification-response")
public ResponseEntity<ApiResponse<FinalSpecialContractDocument>> respondToModificationRequest(
@PathVariable Long contractChatId,
@RequestBody FinalContractModificationResponseDto responseDto,
Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
return ResponseEntity.badRequest()
.body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
}
FinalSpecialContractDocument result =
contractChatService.respondToModificationRequest(
contractChatId, userId, responseDto);
String message = responseDto.isAccepted() ? "수정 요청을 수락했습니다." : "수정 요청을 거절했습니다.";
return ResponseEntity.ok(ApiResponse.success(result, message));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest()
.body(ApiResponse.error("RESPONSE_FAILED", e.getMessage()));
} catch (Exception e) {
log.error("수정 요청 응답 실패", e);
return ResponseEntity.internalServerError()
.body(ApiResponse.error("INTERNAL_ERROR", "응답 처리 중 오류가 발생했습니다."));
}
}
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
around lines 749 to 774, the respondToModificationRequest endpoint is missing an
access control check; before calling
contractChatService.respondToModificationRequest retrieve/verify the
authenticated user's permission for the given contractChatId (e.g., call an
existing service method to validate participation/ownership or fetch the chat
and compare userId to allowed parties) and if the user is unauthorized return a
403 (or throw an AccessDeniedException translated to 403); ensure the controller
does not proceed to process the response unless the access check passes so only
authorized participants can accept/reject modification requests.

Comment on lines +832 to +846
@Override
@PostMapping("/{contractChatId}/final-contract/request-confirmation")
public ResponseEntity<ApiResponse<String>> requestFinalContractConfirmation(
@PathVariable Long contractChatId, Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
contractChatService.requestFinalContractConfirmation(contractChatId, userId);

return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다."));
} catch (Exception e) {
log.error("최종 특약 확정 요청 실패", e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("최종 특약 확정 요청에 실패했습니다: " + e.getMessage()));
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

최종 특약 확정 요청: 접근권한 체크 누락 및 경로 컨벤션 일관성 제안

  • 접근 제어 추가 필요.
  • 경로 패턴이 일부는 "/final-contract/{contractChatId}/..." 일부는 "/{contractChatId}/final-contract/..."로 혼재. 일관된 prefix 유지 권장.

수정안(diff):

   public ResponseEntity<ApiResponse<String>> requestFinalContractConfirmation(
           @PathVariable Long contractChatId, Authentication authentication) {
       try {
           Long userId = getUserIdFromAuthentication(authentication);
+          if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+              return ResponseEntity.badRequest()
+                  .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+          }
           contractChatService.requestFinalContractConfirmation(contractChatId, userId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
@PostMapping("/{contractChatId}/final-contract/request-confirmation")
public ResponseEntity<ApiResponse<String>> requestFinalContractConfirmation(
@PathVariable Long contractChatId, Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
contractChatService.requestFinalContractConfirmation(contractChatId, userId);
return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다."));
} catch (Exception e) {
log.error("최종 특약 확정 요청 실패", e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("최종 특약 확정 요청에 실패했습니다: " + e.getMessage()));
}
}
@Override
@PostMapping("/{contractChatId}/final-contract/request-confirmation")
public ResponseEntity<ApiResponse<String>> requestFinalContractConfirmation(
@PathVariable Long contractChatId, Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
return ResponseEntity.badRequest()
.body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
}
contractChatService.requestFinalContractConfirmation(contractChatId, userId);
return ResponseEntity.ok(ApiResponse.success("최종 특약 확정 요청이 임차인에게 전송되었습니다."));
} catch (Exception e) {
log.error("최종 특약 확정 요청 실패", e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("최종 특약 확정 요청에 실패했습니다: " + e.getMessage()));
}
}
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
around lines 832-846, the endpoint lacks an access control check and its path
pattern conflicts with other routes; add an authorization check (e.g., validate
the caller is the contract participant/owner by calling a service method or add
@PreAuthorize to ensure proper role/ownership) before invoking
contractChatService.requestFinalContractConfirmation, and change the
@PostMapping to follow the chosen project convention consistently (for example,
use @PostMapping("/final-contract/{contractChatId}/request-confirmation") or
update all similar endpoints to the "/{contractChatId}/final-contract/..."
pattern across the controller) so routes are uniform.

Comment on lines +848 to +865
@Override
@PostMapping("/final-contract/{contractChatId}/deletion-request/{clauseOrder}")
public ResponseEntity<ApiResponse<String>> requestFinalContractDeletion(
Long contractChatId, Integer clauseOrder, Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
contractChatService.requestFinalContractDeletion(contractChatId, userId, clauseOrder);

return ResponseEntity.ok(
ApiResponse.success(
String.format("특약 %d번 삭제 요청이 임차인에게 전송되었습니다.", clauseOrder)));
} catch (Exception e) {
log.error("최종 특약 삭제 요청 실패", e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("최종 특약 삭제 요청에 실패했습니다: " + e.getMessage()));
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

PathVariable 바인딩 누락으로 런타임 오류 위험 + 접근권한 체크 누락

메서드 시그니처에 @PathVariable이 없어 경로 변수 바인딩 실패(400/500)가 발생합니다. 또한 접근 제어 추가가 필요합니다.

수정안(diff):

-  public ResponseEntity<ApiResponse<String>> requestFinalContractDeletion(
-          Long contractChatId, Integer clauseOrder, Authentication authentication) {
+  public ResponseEntity<ApiResponse<String>> requestFinalContractDeletion(
+          @PathVariable Long contractChatId,
+          @PathVariable Integer clauseOrder,
+          Authentication authentication) {
       try {
           Long userId = getUserIdFromAuthentication(authentication);
+          if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
+              return ResponseEntity.badRequest()
+                  .body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
+          }
           contractChatService.requestFinalContractDeletion(contractChatId, userId, clauseOrder);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
@PostMapping("/final-contract/{contractChatId}/deletion-request/{clauseOrder}")
public ResponseEntity<ApiResponse<String>> requestFinalContractDeletion(
Long contractChatId, Integer clauseOrder, Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
contractChatService.requestFinalContractDeletion(contractChatId, userId, clauseOrder);
return ResponseEntity.ok(
ApiResponse.success(
String.format("특약 %d번 삭제 요청이 임차인에게 전송되었습니다.", clauseOrder)));
} catch (Exception e) {
log.error("최종 특약 삭제 요청 실패", e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("최종 특약 삭제 요청에 실패했습니다: " + e.getMessage()));
}
}
@Override
@PostMapping("/final-contract/{contractChatId}/deletion-request/{clauseOrder}")
public ResponseEntity<ApiResponse<String>> requestFinalContractDeletion(
@PathVariable Long contractChatId,
@PathVariable Integer clauseOrder,
Authentication authentication) {
try {
Long userId = getUserIdFromAuthentication(authentication);
if (!contractChatService.isUserInContractChat(contractChatId, userId)) {
return ResponseEntity.badRequest()
.body(ApiResponse.error("ACCESS_DENIED", "해당 계약 채팅방에 접근 권한이 없습니다."));
}
contractChatService.requestFinalContractDeletion(contractChatId, userId, clauseOrder);
return ResponseEntity.ok(
ApiResponse.success(
String.format("특약 %d번 삭제 요청이 임차인에게 전송되었습니다.", clauseOrder)));
} catch (Exception e) {
log.error("최종 특약 삭제 요청 실패", e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("최종 특약 삭제 요청에 실패했습니다: " + e.getMessage()));
}
}
🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/chat/controller/ContractChatControllerImpl.java
around lines 848-865, the controller method is missing @PathVariable annotations
(so path variables won't bind) and lacks an access control check; add
@PathVariable("contractChatId") to the contractChatId parameter and
@PathVariable("clauseOrder") to the clauseOrder parameter, then enforce
authorization either with a method-level security annotation (e.g.
@PreAuthorize) or by calling a service check (e.g.
contractChatService.verifyUserAccess(contractChatId, userId)) after resolving
userId and before performing the deletion request, throwing or returning a 403
response on unauthorized access.

Comment on lines +53 to +57
@Value("${front.base.url}")
private String baseUrl;

private String contractChatUrl = "/contract/";

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

하드코딩된 URL 패턴을 설정 파일로 관리하세요

프론트엔드 URL과 계약 채팅 경로가 하드코딩되어 있습니다. 이를 application.yml에서 관리하면 환경별 배포가 용이합니다.

-    @Value("${front.base.url}")
-    private String baseUrl;
-
-    private String contractChatUrl = "/contract/";
+    @Value("${front.base.url}")
+    private String baseUrl;
+    
+    @Value("${front.contract-chat.path:/contract/}")
+    private String contractChatUrl;

application.yml에 추가:

front:
  contract-chat:
    path: /contract/
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
around lines 53 to 57, the contractChatUrl string is hardcoded; move this value
into configuration by adding front.contract-chat.path to application.yml as
suggested and inject it into the class (e.g. via
@Value("${front.contract-chat.path}") or a @ConfigurationProperties bean),
replace the hardcoded field with the injected property, and update any usages to
reference the injected variable so the path can be managed per environment.

Comment on lines +2541 to +2558
public String getContractChatRoomUrl(Long chatRoomId) {
ChatRoom chatRoom = chatRoomMapper.findById(chatRoomId);
if (chatRoom == null) {
log.error("채팅방을 찾을 수 없음: {}", chatRoomId);
throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND);
}
ContractChat contractChatId =
contractChatMapper.findByUserAndHome(
chatRoom.getOwnerId(), chatRoom.getBuyerId(), chatRoom.getHomeId());
if (contractChatId == null) {
log.error("채팅방을 찾을 수 없음: {}", chatRoomId);
throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND);
}
Long contractChatRoomId = contractChatId.getContractChatId();
String param = getContractChatStatus(contractChatId.getStatus());

return baseUrl + contractChatUrl + contractChatRoomId.toString() + param;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

프론트엔드 URL 생성 로직을 개선하세요

URL이 하드코딩되어 있고, null 체크가 불충분합니다.

     public String getContractChatRoomUrl(Long chatRoomId) {
         ChatRoom chatRoom = chatRoomMapper.findById(chatRoomId);
         if (chatRoom == null) {
             log.error("채팅방을 찾을 수 없음: {}", chatRoomId);
             throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND);
         }
+        
         ContractChat contractChatId =
                 contractChatMapper.findByUserAndHome(
                         chatRoom.getOwnerId(), chatRoom.getBuyerId(), chatRoom.getHomeId());
         if (contractChatId == null) {
-            log.error("채팅방을 찾을 수 없음: {}", chatRoomId);
-            throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND);
+            log.error("계약 채팅을 찾을 수 없음 - chatRoomId: {}, ownerId: {}, buyerId: {}, homeId: {}", 
+                chatRoomId, chatRoom.getOwnerId(), chatRoom.getBuyerId(), chatRoom.getHomeId());
+            throw new BusinessException(ChatErrorCode.CONTRACT_CHAT_NOT_FOUND);
         }
+        
         Long contractChatRoomId = contractChatId.getContractChatId();
         String param = getContractChatStatus(contractChatId.getStatus());
 
         return baseUrl + contractChatUrl + contractChatRoomId.toString() + param;
     }

다음 스크립트를 실행하여 CONTRACT_CHAT_NOT_FOUND 에러 코드가 정의되어 있는지 확인하세요:


🏁 Script executed:

#!/bin/bash
# ChatErrorCode enum에서 CONTRACT_CHAT_NOT_FOUND 확인
rg -A 5 "enum ChatErrorCode" --type java
rg "CONTRACT_CHAT_NOT_FOUND" --type java

Length of output: 872


프론트엔드 URL 생성 로직 개선 및 에러 처리 강화

baseUrlcontractChatUrl을 애플리케이션 설정으로 외부화하고, UriComponentsBuilder 등을 사용해 URL을 안전하게 조립하세요. 또한, contractChat 미조회 시 로깅 메시지에 상세 정보를 추가하고, 정의된 CONTRACT_CHAT_NOT_FOUND 에러 코드를 사용하도록 수정합니다.

적용 대상:

  • 파일: src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java (2541–2558)

제안된 변경(diff):

         if (contractChatId == null) {
-            log.error("채팅방을 찾을 수 없음: {}", chatRoomId);
-            throw new BusinessException(ChatErrorCode.CHAT_ROOM_NOT_FOUND);
+            log.error(
+                "계약 채팅을 찾을 수 없음 - chatRoomId: {}, ownerId: {}, buyerId: {}, homeId: {}",
+                chatRoomId, chatRoom.getOwnerId(), chatRoom.getBuyerId(), chatRoom.getHomeId()
+            );
+            throw new BusinessException(ChatErrorCode.CONTRACT_CHAT_NOT_FOUND);
         }
🤖 Prompt for AI Agents
In src/main/java/org/scoula/domain/chat/service/ContractChatServiceImpl.java
around lines 2541-2558, the method builds the frontend URL by concatenating
baseUrl and contractChatUrl directly and logs the wrong error/insufficient
details when contractChat lookup fails; change baseUrl and contractChatUrl to be
injected from application settings (e.g., @Value or configuration properties),
use UriComponentsBuilder (or equivalent) to safely build the URL with path
segments and query params instead of simple string concatenation, enhance the
null-case logging to include chatRoomId, ownerId, buyerId and homeId, and throw
the defined CONTRACT_CHAT_NOT_FOUND BusinessException constant when contractChat
is null; ensure contractChatId is used safely (null-check) before calling
getContractChatId and return the constructed URI.toString().

Comment on lines +20 to +21
Optional<Long> selectContractOwnerId(@Param("contractChatId") Long contractChatId);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

계약 소유자 조회 메서드 추가 적절. 네이밍 컨벤션만 정리 제안

Optional selectContractOwnerId(Long contractChatId) 도입은 ContractService의 소유자 검증에 적합합니다.

  • 네이밍: 동일 블록에 selectContractBuyerId, selectBuyerId가 공존해 의미가 혼재됩니다. 일관성을 위해
    • selectContractBuyerId ↔ selectContractOwnerId (contract_chat 기준)
    • selectBuyerId → selectBuyerIdFromPrecontract 혹은 selectPrecontractBuyerId (precontract 기준)
      와 같이 구분하면 이해가 쉬워집니다.

다음 스크립트로 매퍼 XML/사용처 일치 여부를 확인해 주세요:


🏁 Script executed:

#!/bin/bash
rg -n "selectContractOwnerId\\(" -A 2 -B 2
rg -n "id=\"selectContractOwnerId\"" src/main/resources

Length of output: 5096


네이밍 컨벤션 통일 제안
매퍼 XML과 인터페이스 메서드 매핑에는 문제가 없으므로, 메서드 이름만 일관되게 정리하면 가독성이 크게 향상됩니다.

  • src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java (18–22라인)
    • selectContractBuyerId, selectContractOwnerId: 둘 다 contractChatId 기준이므로 “selectContractBuyerId ↔ selectContractOwnerId” 형태로 유지
    • selectBuyerId: precontract 기준임을 명확히 하기 위해 “selectBuyerIdFromPrecontract” 또는 “selectPrecontractBuyerId”로 변경 제안

위와 같이 네이밍을 통일해 주시면 좋겠습니다.

🤖 Prompt for AI Agents
In
src/main/java/org/scoula/domain/precontract/mapper/TenantPreContractMapper.java
around lines 18–22, the method names should be made consistent: keep the pair
selectContractBuyerId and selectContractOwnerId as they both use contractChatId,
and rename selectBuyerId to a clearer name such as selectPrecontractBuyerId (or
selectBuyerIdFromPrecontract). Update the method signature in this interface to
the new name and then update the corresponding mapper XML / any call sites to
use the new method name so the interface and XML mapping remain in sync.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 이슈 제목

2 participants